This example will show you how to write a service (we will call it dialog manager) that will help you to show dialogs in your MVVM application
We assume you already now how the MVVM pattern works and how dialogs, such as file dialogs, can be shown in general. You should also know what a [TopLevel]-control in Avalonia is and what it can be used for.
ℹ️
|
In this sample we will use the [CommunityToolkit.MVVM]-package which provides source generators for less boilerplate code. Please check out their docs if you want to learn more about this. (https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/overview) |
In our project we add a folder called Services
. Inside we will create a class called DialogManager
.
ℹ️
|
We do not need to inherit AvaloniaObject even as we want to add Register as an AttachedProperty because Avalonia supports AttachedProperties on any object
|
Let’s add a static Dictionary
where we can store the registered mappings between the context (our ViewModel
) and the View
or Control
, which we want to interact with.
public class DialogManager
{
private static readonly Dictionary<object, Visual> RegistrationMapper =
new Dictionary<object, Visual>();
}
In the next step we add an [AttachedProperty] to our dialog manager, which we call Register
. Later we can bind any object to this property from every available Visual
in our Views
.
/// <summary>
/// This property handles the registration of Views and ViewModel
/// </summary>
public static readonly AttachedProperty<object?> RegisterProperty = AvaloniaProperty.RegisterAttached<DialogManager, Visual, object?>(
"Register");
/// <summary>
/// Accessor for Attached property <see cref="RegisterProperty"/>.
/// </summary>
public static void SetRegister(AvaloniaObject element, object value)
{
element.SetValue(RegisterProperty, value);
}
/// <summary>
/// Accessor for Attached property <see cref="RegisterProperty"/>.
/// </summary>
public static object? GetRegister(AvaloniaObject element)
{
return element.GetValue(RegisterProperty);
}
Now that we want to store or remove any new binding into our Dictionary
, we need to listen to changes of the RegsiterProperty
. We can add such a listener inside the static constructor.
static DialogManager()
{
RegisterProperty.Changed.AddClassHandler<Visual>(RegisterChanged);
}
private static void RegisterChanged(Visual sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is null)
{
throw new InvalidOperationException("The DialogManager can only be registered on a Visual");
}
// Unregister any old registered context
if (e.OldValue != null)
{
RegistrationMapper.Remove(e.OldValue);
}
// Register any new context
if (e.NewValue != null)
{
RegistrationMapper.Add(e.NewValue, sender);
}
}
To make our life easier, we add can add some static functions to lookup the registered view.
/// <summary>
/// Gets the associated <see cref="Visual"/> for a given context. Returns null, if none was registered
/// </summary>
/// <param name="context">The context to lookup</param>
/// <returns>The registered Visual for the context or null if none was found</returns>
public static Visual? GetVisualForContext(object context)
{
return RegistrationMapper.TryGetValue(context, out var result) ? result : null;
}
/// <summary>
/// Gets the parent <see cref="TopLevel"/> for the given context. Returns null, if no TopLevel was found
/// </summary>
/// <param name="context">The context to lookup</param>
/// <returns>The registered TopLevel for the context or null if none was found</returns>
public static TopLevel? GetTopLevelForContext(object context)
{
return TopLevel.GetTopLevel(GetVisualForContext(context));
}
If we are even more lazy, we can add some extension methods which we can use later to call a dialog in one line:
/// <summary>
/// A helper class to manage dialogs via extension methods. Add more on your own
/// </summary>
public static class DialogHelper
{
/// <summary>
/// Shows an open file dialog for a registered context, most likely a ViewModel
/// </summary>
/// <param name="context">The context</param>
/// <param name="title">The dialog title or a default is null</param>
/// <param name="selectMany">Is selecting many files allowed?</param>
/// <returns>An array of file names</returns>
/// <exception cref="ArgumentNullException">if context was null</exception>
public static async Task<IEnumerable<string>?> OpenFileDialogAsync(this object? context, string? title = null, bool selectMany = true)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// lookup the TopLevel for the context
var topLevel = DialogService.GetTopLevelForContext(context);
if(topLevel != null)
{
// Open the file dialog
var storageFiles = await topLevel.StorageProvider.OpenFilePickerAsync(
new FilePickerOpenOptions()
{
AllowMultiple = selectMany,
Title = title ?? "Select any file(s)"
});
// return the result
return storageFiles.Select(s => s.Name);
}
return null;
}
}
Now that we have our DialogManager
created, we can start to register the View
for our ViewModel
. Thanks to our attached property, we can simply do:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:services="using:DialogManagerSample.Services"
services:DialogManager.Register="{Binding}">
<!-- Your content goes here -->
</UserControl>
And in the ViewModel
we can use our extension methods anywhere. The below sample command will ask the user to select a bunch of files. The command itself will be created using the source generators that the CommunityToolkit.MVVM-package provides.
[RelayCommand]
private async Task SelectFilesAsync()
{
// Selected Files is a property of our ViewModel
SelectedFiles = await this.OpenFileDialogAsync("Hello Avalonia");
}
Now we can add the needed List
and Button
in our view:
<Grid RowDefinitions="Auto,*,Auto">
<TextBlock Text="Selected Files:" />
<ListBox ItemsSource="{Binding SelectedFiles}" Grid.Row="1" />
<Button Content="Select Files"
Command="{Binding SelectFilesCommand}"
Grid.Row="2" />
</Grid>
There are more ways to show dialogs from the ViewModel, for example: